热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

尾部|柜台_Java并发线程池篇附场景分析

篇首语:本文由编程笔记#小编为大家整理,主要介绍了Java并发-线程池篇-附场景分析相关的知识,希望对你有一定的参考价值。作者:汤圆个人博客

篇首语:本文由编程笔记#小编为大家整理,主要介绍了Java并发-线程池篇-附场景分析相关的知识,希望对你有一定的参考价值。


作者:汤圆

个人博客:javalover.cc


前言

前面我们在创建线程时,都是直接new Thread();

这样短期来看是没有问题的,但是一旦业务量增长,线程数过多,就有可能导致内存异常OOM,CPU爆满等问题

幸运的是,Java里面有线程池的概念,而线程池的核心框架,就是我们今天的主题,Executor

接下来,就让我们一起畅游在Java线程池的海洋中吧



本节会用银行办业务的场景来对比介绍线程池的核心概念,这样理解起来会很轻松



简介

Executor是线程池的核心框架;

和它相对应的有一个辅助工厂类Executors,这个类提供了许多工厂方法,用来创建各种各样的线程池,下面我们先看下几种常见的线程池

// 容量固定的线程池
Executor fixedThreadPool = Executors.newFixedThreadPool(5);
// 容量动态增减的线程池
Executor cachedThreadPool = Executors.newCachedThreadPool();
// 单个线程的线程池
Executor singleThreadExecutor = Executors.newSingleThreadExecutor();
// 基于调度机制的线程池(不同于上面的线程池,这个池创建的任务不会立马执行,而是定期或者延时执行)
Executor scheduledThreadPool = Executors.newScheduledThreadPool(5);

上面这些线程池的区别主要就是线程数量的不同以及任务执行的时机

下面让我们开始吧



文章如果有问题,欢迎大家批评指正,在此谢过啦



目录


  1. 线程池的底层类ThreadPoolExecutor
  2. 为啥阿里不建议使用 Executors来创建线程池?
  3. 线程池的生命周期 ExecutorService

正文


1. 线程池的底层类 ThreadPoolExecutor

在文章开头创建的几个线程池,内部都是有调用ThreadPoolExecutor这个类的,如下所示

public static ExecutorService newFixedThreadPool(int nThreads)
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());

这个类是Exexutor的一个实现类&#xff0c;关系图如下所示&#xff1a;


  • 其中Executors就是上面介绍的辅助工厂类&#xff0c;用来创建各种线程池

  • 接口ExecutorService是Executor的一个子接口&#xff0c;它对Executor进行了扩展&#xff0c;原有的Executor只能执行任务&#xff0c;而ExecutorService还可以管理线程池的生命周期&#xff08;下面会介绍&#xff09;

所以我们先来介绍下这个底层类&#xff0c;它的完整构造参数如下所示&#xff1a;

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)

在介绍这些参数之前&#xff0c;我们可以先举个生活中的例子-去银行办业务&#xff1b;然后对比着来理解&#xff0c;会比较清晰

&#xff08;图中绿色的窗口表示一直开着&#xff09;


  • corePoolSize&#xff1a; 核心线程数&#xff0c;就是一直存在的线程&#xff08;不管用不用&#xff09;&#xff1b;&#61;》窗口的1号窗和2号窗
  • maximumPoolSize&#xff1a;最大线程数&#xff0c;就是最多可以创建多少个线程&#xff1b;&#61;》窗口的1&#xff0c;2&#xff0c;3&#xff0c;4号窗
  • keepAliveTime&#xff1a;多余的线程&#xff08;最大线程数 减去 核心线程数&#xff09;空闲时存活的时间&#xff1b;&#61;》窗口的3号窗和4号窗空闲的时间,如果超过keepAliveTime&#xff0c;还没有人来办业务&#xff0c;那么就会暂时关闭3号窗和4号窗
  • workQueue: 工作队列&#xff0c;当核心线程数都在执行任务时&#xff0c;再进来的任务就会添加到工作队列中&#xff1b;&#61;》椅子&#xff0c;客户等待区
  • threadFactory&#xff1a;线程工厂&#xff0c;用来创建初始的核心线程&#xff0c;下面会有介绍&#xff1b;
  • handler&#xff1a;拒绝策略&#xff0c;当所有线程都在执行任务&#xff0c;且工作队列也满时&#xff0c;再进来的任务就会被执行拒绝策略&#xff08;比如丢弃&#xff09;&#xff1b;&#61;》左下角的那个小人

基本的工作流程如下所示&#xff1a;

上面的参数我们着重介绍下工作队列和拒绝策略&#xff0c;线程工厂下面再介绍

工作队列&#xff1a;


  • ArrayBlockingQueue&#xff1a;
    • 数组阻塞队列&#xff0c;这个队列是一个有界队列&#xff0c;遵循FIFO&#xff0c;尾部插入&#xff0c;头部获取
    • 初始化时需指定队列的容量 capacity
    • 类比到上面的场景&#xff0c;就是椅子的数量为初始容量capacity
  • LinkedBlockingQueue&#xff1a;
    • 链表阻塞队列&#xff0c;这是一个无界队列&#xff0c;遵循FIFO&#xff0c;尾部插入&#xff0c;头部获取
    • 初始化时可不指定容量&#xff0c;此时默认的容量为Integer.MAX_VALUE&#xff0c;基本上相当于无界了&#xff0c;此时队列可一直插入&#xff08;如果处理任务的速度小于插入的速度&#xff0c;时间长了就有可能导致OOM)
    • 类比到上面的场景&#xff0c;就是椅子的数量为Integer.MAX_VALUE
  • SynchronousQueue&#xff1a;
    • 同步队列&#xff0c;阻塞队列的特殊版&#xff0c;即没有容量的阻塞队列&#xff0c;随进随出&#xff0c;不做停留
    • 类比到上面的场景&#xff0c;就是椅子的数量为0&#xff0c;来一个人就去柜台办理&#xff0c;如果柜台满了&#xff0c;就拒绝
  • PriorityBlockingQueue
    • 优先级阻塞队列&#xff0c;这是一个无界队列&#xff0c;不遵循FIFO&#xff0c;而是根据任务自身的优先级顺序来执行
    • 初始化可不指定容量&#xff0c;默认11&#xff08;既然有容量&#xff0c;怎么还是无界的呢&#xff1f;因为它添加元素时会进行扩容&#xff09;
    • 类比到上面的场景&#xff0c;就是新来的可以插队办理业务&#xff0c;好比各种会员

拒绝策略&#xff1a;


  • AbortPolicy&#xff08;默认&#xff09;&#xff1a;
    • 中断策略&#xff0c;抛出异常 RejectedExecutionException&#xff1b;
    • 如果线程数达到最大&#xff0c;且工作队列也满&#xff0c;此时再进来任务&#xff0c;则抛出 RejectedExecutionException&#xff08;系统会停止运行&#xff0c;但是不会退出&#xff09;
  • DiscardPolicy&#xff1a;
    • 丢弃策略&#xff0c;丢掉新来的任务
    • 如果线程数达到最大&#xff0c;且工作队列也满&#xff0c;此时再进来任务&#xff0c;则直接丢掉&#xff08;看任务的重要程度&#xff0c;不重要的任务可以用这个策略&#xff09;
  • DiscardOldestPolicy&#xff1a;
    • 丢弃最旧策略&#xff0c;丢掉最先进入队列的任务&#xff08;有点残忍了&#xff09;&#xff0c;然后再次执行插入操作
    • 如果线程数达到最大&#xff0c;且工作队列也满&#xff0c;此时再进来任务&#xff0c;则直接丢掉队列头部的任务&#xff0c;并再次插入任务
  • CallerRunsPolicy&#xff1a;
    • 回去执行策略&#xff0c;让新来的任务返回到调用它的线程中去执行&#xff08;比如main线程调用了executors.execute(task)&#xff0c;那么就会将task返回到main线程中去执行&#xff09;
    • 如果线程数达到最大&#xff0c;且工作队列也满&#xff0c;此时再进来任务&#xff0c;则直接返回该任务&#xff0c;到调用它的线程中去执行

2. 为啥阿里不建议使用 Executors来创建线程池&#xff1f;

原话如下&#xff1a;

我们可以写几个代码来测试一下

先测试FixedThreadPool&#xff0c;代码如下&#xff1a;

public class FixedThreadPoolDemo
public static void main(String[] args)
// 创建一个固定容量为10的线程池&#xff0c;核心线程数和最大线程数都为10
ExecutorService executorService &#61; Executors.newFixedThreadPool(10);
for (int i &#61; 0; i < 1_000_000; i&#43;&#43;)
try
executorService.execute(()->
try
Thread.sleep(1000);
catch (InterruptedException e)
e.printStackTrace();

);
catch (Exception e)
e.printStackTrace();




这里我们需对VM参数做一点修改&#xff0c;让问题比较容易复现

如下所示&#xff0c;我们添加-Xmx8m -Xms8m到VM option中(-Xmx8m&#xff1a;JVM堆的最大内存为8M&#xff0c; -Xms8m&#xff0c;JVM堆的初始化内存为8M)&#xff1a;

此时点击运行&#xff0c;就会发现报错如下&#xff1a;

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.concurrent.LinkedBlockingQueue.offer(LinkedBlockingQueue.java:416)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1371)
at com.jalon.concurrent.chapter6.FixedThreadPoolDemo.main(FixedThreadPoolDemo.java:21)

我们来分析下原因


  • 首先&#xff0c;newFixedThreadPool内部用的工作队列为LinkedBlockingQueue&#xff0c;这是一个无界队列&#xff08;容量最大为Integer.MAX_VALUE&#xff0c;基本上可一直添加任务&#xff09;
  • 如果任务插入的速度&#xff0c;超过了任务执行的速度&#xff0c;那么队列肯定会越来越长&#xff0c;最终导致OOM


CachedThreadPool也是类似的原因&#xff0c;只不过它是因为最大线程数为Integer.MAX_VALUE&#xff1b;


所以当任务插入的速度&#xff0c;超过了任务执行的速度&#xff0c;那么线程的数量会越来越多&#xff0c;最终导致OOM


那我们要怎么创建线程池呢&#xff1f;

可以用ThreadPoolExecutor来自定义创建&#xff0c;通过为最大线程数和工作队列都设置一个边界&#xff0c;来限制相关的数量&#xff0c;如下所示&#xff1a;

public class ThreadPoolExecutorDemo
public static void main(String[] args)
ExecutorService service &#61; new ThreadPoolExecutor(
1, // 核心线程数
1, // 最大线程数
60L, // 空闲时间
TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(1), // 数组工作队列&#xff0c;长度1
new ThreadPoolExecutor.DiscardPolicy()); // 拒绝策略&#xff1a;丢弃
for (int i &#61; 0; i < 1_000_000; i&#43;&#43;)
// 通过这里的打印信息&#xff0c;我们可以知道循环了3次
// 原因就是第一次的任务在核心线程中执行&#xff0c;第二次的任务放到了工作队列&#xff0c;第三次的任务被拒绝执行
System.out.println(i);
service.execute(()->
// 这里会报异常&#xff0c;是因为执行了拒绝策略&#xff08;达到了最大线程数&#xff0c;队列也满了&#xff0c;此时新进来的任务就会执行拒绝策略&#xff09;
// 这里需要注意的是&#xff0c;抛出异常后&#xff0c;代码并不会退出&#xff0c;而是卡在异常这里&#xff0c;包括主线程也会被卡住(这个是默认的拒绝策略&#xff09;
// 我们可以用其他的拒绝策略&#xff0c;比如DiscardPolicy,此时代码就会继续往下执行
System.out.println(Thread.currentThread().getName());
);

try
Thread.sleep(1000);
System.out.println("主线程 sleep ");
catch (InterruptedException e)
e.printStackTrace();




3. 线程池的生命周期 ExecutorService

Executor接口默认只有一个方法void execute(Runnable command);&#xff0c;用来执行任务

任务一旦开启&#xff0c;我们就无法再去插手了&#xff0c;比如停止、监控等

此时就需要ExecutorService登场了&#xff0c;它是Executor的一个子接口&#xff0c;对其进行了扩展&#xff0c;方法如下&#xff1a;

public interface ExecutorService extends Executor
void shutdown(); // 优雅地关闭&#xff0c;这个关闭会持续一段时间&#xff0c;以等待已经提交的任务去执行完成&#xff08;但是在shutdown之后提交的任务会被拒绝&#xff09;
List<Runnable> shutdownNow(); // 粗暴地关闭&#xff0c;这个关闭会立即关闭所有正在执行的任务&#xff0c;并返回工作队列中等待的任务
boolean isShutdown();
boolean isTerminated();
// 用来等待线程的执行
// 如果在timeout之内&#xff0c;线程都执行完了&#xff0c;则返回true&#xff1b;
// 如果等了timeout&#xff0c;还没执行完&#xff0c;则返回false&#xff1b;
// 如果timeout之内&#xff0c;线程被中断&#xff0c;则抛出中断异常
boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException;
<T> Future<T> submit(Callable<T> task);

<T> Future<T> submit(Runnable task, T result);

从上面可以看到&#xff0c;线程池的生命周期分三步&#xff1a;


  1. 运行&#xff1a;创建后就开始运行
  2. 关闭&#xff1a;调用shutdown进入关闭状态
  3. 已终止&#xff1a;所有线程执行完毕

总结


  1. 线程池的底层类 ThreadPoolExecutor&#xff1a;核心概念就是核心线程数、最大线程数、工作队列、拒绝策略
  2. 为啥阿里不建议使用 Executors来创建线程池&#xff1f;&#xff1a;因为会导致OOM&#xff0c;解决办法就是自定义ThreadPoolExecutor&#xff0c;为最大线程数和工作队列设置边界
  3. 线程池的生命周期ExecutorService&#xff1a;运行状态&#xff08;创建后进入&#xff09;、关闭状态&#xff08;shutdown后进入&#xff09;、已终止状态&#xff08;所有线程都执行完成后进入&#xff09;

参考内容&#xff1a;


  • 《Java并发编程实战》
  • 《实战Java高并发》
  • newFixedThreadPool的弊端&#xff1a;https://my.oschina.net/langwanghuangshifu/blog/3208320
  • 银行办业务的场景参考&#xff1a;https://b23.tv/ygGjTH

后记

愿你的意中人亦是中意你之人


推荐阅读
  • 并发编程 12—— 任务取消与关闭 之 shutdownNow 的局限性
    Java并发编程实践目录并发编程01——ThreadLocal并发编程02——ConcurrentHashMap并发编程03——阻塞队列和生产者-消费者模式并发编程04——闭锁Co ... [详细]
  • 本文介绍了如何在 C# 和 XNA 框架中实现一个自定义的 3x3 矩阵类(MMatrix33),旨在深入理解矩阵运算及其应用场景。该类参考了 AS3 Starling 和其他相关资源,以确保算法的准确性和高效性。 ... [详细]
  • Startup 类配置服务和应用的请求管道。Startup类ASP.NETCore应用使用 Startup 类,按照约定命名为 Startup。 Startup 类:可选择性地包括 ... [详细]
  • 在维护公司项目时,发现按下手机的某个物理按键后会激活相应的服务,并在屏幕上模拟点击特定坐标点。本文详细介绍了如何使用ADB Shell Input命令来模拟各种输入事件,包括滑动、按键和点击等。 ... [详细]
  • andr ... [详细]
  • 并发编程:深入理解设计原理与优化
    本文探讨了并发编程中的关键设计原则,特别是Java内存模型(JMM)的happens-before规则及其对多线程编程的影响。文章详细介绍了DCL双重检查锁定模式的问题及解决方案,并总结了不同处理器和内存模型之间的关系,旨在为程序员提供更深入的理解和最佳实践。 ... [详细]
  • 本文提供了使用Java实现Bellman-Ford算法解决POJ 3259问题的代码示例,详细解释了如何通过该算法检测负权环来判断时间旅行的可能性。 ... [详细]
  • 本文详细探讨了JDBC(Java数据库连接)的内部机制,重点分析其作为服务提供者接口(SPI)框架的应用。通过类图和代码示例,展示了JDBC如何注册驱动程序、建立数据库连接以及执行SQL查询的过程。 ... [详细]
  • 深入探讨CPU虚拟化与KVM内存管理
    本文详细介绍了现代服务器架构中的CPU虚拟化技术,包括SMP、NUMA和MPP三种多处理器结构,并深入探讨了KVM的内存虚拟化机制。通过对比不同架构的特点和应用场景,帮助读者理解如何选择最适合的架构以优化性能。 ... [详细]
  • 本文探讨了领域驱动设计(DDD)的核心概念、应用场景及其实现方式,详细介绍了其在企业级软件开发中的优势和挑战。通过对比事务脚本与领域模型,展示了DDD如何提升系统的可维护性和扩展性。 ... [详细]
  • 本文介绍了几种不同的编程方法来计算从1到n的自然数之和,包括循环、递归、面向对象以及模板元编程等技术。每种方法都有其特点和适用场景。 ... [详细]
  • 微软Exchange服务器遭遇2022年版“千年虫”漏洞
    微软Exchange服务器在新年伊始遭遇了一个类似于‘千年虫’的日期处理漏洞,导致邮件传输受阻。该问题主要影响配置了FIP-FS恶意软件引擎的Exchange 2016和2019版本。 ... [详细]
  • Redis Hash 数据结构详解
    本文详细介绍了 Redis 中的 Hash 数据类型及其常用命令。Hash 类型用于存储键值对集合,支持多种操作如插入、查询、更新和删除字段值。此外,文章还探讨了 Hash 类型在实际业务场景中的应用,并提供了优化建议。 ... [详细]
  • 丽江客栈选择问题
    本文介绍了一道经典的算法题,题目涉及在丽江河边的n家特色客栈中选择住宿方案。两位游客希望住在色调相同的两家客栈,并在晚上选择一家最低消费不超过p元的咖啡店小聚。我们将详细探讨如何计算满足条件的住宿方案总数。 ... [详细]
  • 由二叉树到贪心算法
    二叉树很重要树是数据结构中的重中之重,尤其以各类二叉树为学习的难点。单就面试而言,在 ... [详细]
author-avatar
子新宥梅93
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有